package com.abso.mp3tunes.locker.ui.views;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.tabbed.ITabbedPropertySheetPageContributor;
import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;

import com.abso.mp3tunes.locker.core.data.LockerDataUtils;
import com.abso.mp3tunes.locker.core.data.Track;
import com.abso.mp3tunes.locker.ui.ILockerConstants;
import com.abso.mp3tunes.locker.ui.IMP3PlayerListener;
import com.abso.mp3tunes.locker.ui.LockerImages;
import com.abso.mp3tunes.locker.ui.LockerPlugin;
import com.abso.mp3tunes.locker.ui.MP3Player;
import com.abso.mp3tunes.locker.ui.dnd.LockerTransfer;
import com.abso.mp3tunes.locker.ui.viewers.LockerViewer;

/**
 * The music player view contributes to the workbench with a MP3 player.
 */
public class MusicPlayerView extends ViewPart implements IMP3PlayerListener,
		ITabbedPropertySheetPageContributor, ISelectionProvider {

	/** The index of the previous button. */
	private static final int PREV_BUTTON_INDEX = 0;

	/** The index of the play button. */
	private static final int PLAY_BUTTON_INDEX = 1;

	/** The index of the stop button. */
	private static final int STOP_BUTTON_INDEX = 2;

	/** The index of the next button. */
	private static final int NEXT_BUTTON_INDEX = 3;

	/** The index of the shuffle button. */
	private static final int SHUFFLE_BUTTON_INDEX = 4;

	/** The index of the repeat button. */
	private static final int REPEAT_BUTTON_INDEX = 5;

	/** The label showing the title of the current track. */
	private Label titleLabel;

	/** The control buttons. */
	private Button[] buttons;

	/** The control displaying the album art for the current track. */
	private Canvas canvas;

	/** The list of enqueued tracks. */
	private List tracks = new ArrayList();

	/** Indices to be used to retrieve tracks, in shuffle mode. */
	private List shuffledIndices = new ArrayList();

	/** The current track index. */
	private int trackIndex = 0;

	/** The MP3 player thread. */
	private MP3Player mp3Player;

	/** The list of selection changed listeners. */
	private ListenerList selectionChangedListeners = new ListenerList();

	/** The thread which loads and displays the album art image. */
	private LoadAlbumArtThread albumArtThread;

	/** The last played track (for AudioScrobbler submission). */
	private Track lastTrack;
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.part.WorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
	 */
	public void createPartControl(Composite parent) {
		Composite mainComp = new Composite(parent, SWT.NONE);
		mainComp.setLayout(new GridLayout(1, true));
		titleLabel = new Label(mainComp, SWT.BORDER);
		titleLabel
				.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		createButtonControl(mainComp);
		int ops = DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_DEFAULT;
		DropTarget target = new DropTarget(mainComp, ops);
		target.setTransfer(new Transfer[] { LockerTransfer.INSTANCE });
		target.addDropListener(new DropTargetAdapter() {
			public void dragEnter(DropTargetEvent event) {
				if (event.detail == DND.DROP_DEFAULT) {
					event.detail = DND.DROP_COPY;
				}
			}

			public void dragOperationChanged(DropTargetEvent event) {
				if (event.detail == DND.DROP_DEFAULT) {
					event.detail = DND.DROP_COPY;
				}
			}

			public void dropAccept(DropTargetEvent event) {
			}

			public void drop(DropTargetEvent event) {
				if (event.data instanceof LockerViewer) {
					LockerViewer lockerViewer = (LockerViewer) event.data;
					lockerViewer.playSelection();
				}
			}
		});
	}

	/**
	 * Creates the button composite control.
	 * 
	 * @param parent
	 *            the parent control.
	 */
	private void createButtonControl(Composite parent) {
		Composite buttonComp = new Composite(parent, SWT.NONE);
		buttonComp
				.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		RowLayout layout = new RowLayout();
		layout.fill = false;
		buttonComp.setLayout(layout);
		buttons = new Button[6];

		/* creates buttons */
		buttons[PREV_BUTTON_INDEX] = createButton(buttonComp,
				LockerImages.PREVIOUS_TRACK, "Previous Track");
		buttons[PLAY_BUTTON_INDEX] = createButton(buttonComp,
				LockerImages.PLAY_TRACK, "Play");
		buttons[STOP_BUTTON_INDEX] = createButton(buttonComp,
				LockerImages.STOP_TRACK, "Stop");
		buttons[NEXT_BUTTON_INDEX] = createButton(buttonComp,
				LockerImages.NEXT_TRACK, "Next Track");
		new Label(buttonComp, SWT.NONE).setText("   ");
		buttons[SHUFFLE_BUTTON_INDEX] = createToggleButton(buttonComp,
				LockerImages.SHUFFLE, "Shuffle");
		buttons[SHUFFLE_BUTTON_INDEX].setSelection(isShuffleEnabled());
		buttons[REPEAT_BUTTON_INDEX] = createToggleButton(buttonComp,
				LockerImages.REPEAT, "Repeat");
		buttons[REPEAT_BUTTON_INDEX].setSelection(isRepeatEnabled());

		/* creates the album art canvas */
		canvas = new Canvas(buttonComp, SWT.NONE);
		RowData rowData = new RowData(150, 150);
		canvas.setLayoutData(rowData);

		/* adds listeners to buttons */
		buttons[PREV_BUTTON_INDEX].addSelectionListener(new SelectionAdapter() {

			public void widgetSelected(SelectionEvent e) {
				if ((trackIndex == 0) && (isRepeatEnabled())) {
					trackIndex = tracks.size();
				}
				if (trackIndex > 0) {
					trackIndex--;
					play();
				}
			}

		});
		buttons[PLAY_BUTTON_INDEX].addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				if (buttons[PLAY_BUTTON_INDEX].getToolTipText().equals("Play")) {
					play();
				} else {
					pause();
				}
			}

		});
		buttons[STOP_BUTTON_INDEX].addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				stop();
			}

		});
		buttons[NEXT_BUTTON_INDEX].addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				if ((trackIndex == (tracks.size() - 1)) && (isRepeatEnabled())) {
					trackIndex = -1;
				}
				if (trackIndex < (tracks.size() - 1)) {
					trackIndex++;
					play();
				}
			}
		});
		buttons[SHUFFLE_BUTTON_INDEX]
				.addSelectionListener(new SelectionAdapter() {
					public void widgetSelected(SelectionEvent e) {
						LockerPlugin.getDefault().getPreferenceStore()
								.setValue(
										"playerShuffle",
										buttons[SHUFFLE_BUTTON_INDEX]
												.getSelection());
						if ((trackIndex >= 0)
								&& (trackIndex < shuffledIndices.size())) {
							if (isShuffleEnabled()) {
								int shuffledIndex = shuffledIndices
										.indexOf(new Integer(trackIndex));
								if (shuffledIndex != -1) {
									Collections.swap(shuffledIndices, 0,
											shuffledIndex);
									trackIndex = 0;
								}
							} else {
								trackIndex = ((Integer) shuffledIndices
										.get(trackIndex)).intValue();
							}
						}
					}
				});
		buttons[REPEAT_BUTTON_INDEX]
				.addSelectionListener(new SelectionAdapter() {
					public void widgetSelected(SelectionEvent e) {
						LockerPlugin.getDefault().getPreferenceStore()
								.setValue(
										"playerRepeat",
										buttons[REPEAT_BUTTON_INDEX]
												.getSelection());
					}
				});
	}

	/**
	 * Returns whether the play queue is repeated.
	 * 
	 * @return <code>true</code> if the repeat preference is set.
	 */
	private boolean isRepeatEnabled() {
		return LockerPlugin.getDefault().getPreferenceStore().getBoolean(
				"playerRepeat");
	}

	/**
	 * Returns whether the play queue is shuffled.
	 * 
	 * @return <code>true</code> if the shuffle preference is set.
	 */
	private boolean isShuffleEnabled() {
		return LockerPlugin.getDefault().getPreferenceStore().getBoolean(
				"playerShuffle");
	}

	/**
	 * Creates a player button.
	 * 
	 * @param parent
	 *            the parent composite.
	 * @param imageKey
	 *            the image key (see {@link LockerImages}).
	 * @param tooltipText
	 *            the tooltip text.
	 * @return the created button.
	 */
	private Button createButton(Composite parent, String imageKey,
			String tooltipText) {
		Button button = new Button(parent, SWT.PUSH);
		button.setImage(LockerImages.get(imageKey));
		button.setToolTipText(tooltipText);
		return button;
	}

	/**
	 * Creates a player toggle button.
	 * 
	 * @param parent
	 *            the parent composite.
	 * @param imageKey
	 *            the image key (see {@link LockerImages}).
	 * @param tooltipText
	 *            the tooltip text.
	 * @return the created button.
	 */
	private Button createToggleButton(Composite parent, String imageKey,
			String tooltipText) {
		Button button = new Button(parent, SWT.TOGGLE);
		button.setImage(LockerImages.get(imageKey));
		button.setToolTipText(tooltipText);
		return button;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.part.ViewPart#init(org.eclipse.ui.IViewSite)
	 */
	public void init(IViewSite site) throws PartInitException {
		site.setSelectionProvider(this);
		super.init(site);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.views.properties.tabbed.ITabbedPropertySheetPageContributor#getContributorId()
	 */
	public String getContributorId() {
		return ILockerConstants.ID_PROPERTY_CONTRIBUTOR;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.part.WorkbenchPart#setFocus()
	 */
	public void setFocus() {
		buttons[PLAY_BUTTON_INDEX].setFocus();
	}

	/**
	 * Sets the current tracks queue.
	 * 
	 * @param tracks
	 *            the current tracks.
	 */
	public void setTracks(List tracks) {
		this.tracks = new ArrayList(tracks);
		this.shuffledIndices = new ArrayList(tracks.size());
		for (int i = 0; i < tracks.size(); i++) {
			shuffledIndices.add(new Integer(i));
		}
		Collections.shuffle(shuffledIndices);
		trackIndex = 0;
	}

	/** Plays the current song. */
	public void play() {
		buttons[PLAY_BUTTON_INDEX].setToolTipText("Pause");
		buttons[PLAY_BUTTON_INDEX].setImage(LockerImages
				.get(LockerImages.PAUSE_TRACK));
		if (mp3Player != null) {
			if (mp3Player.isPaused()) {
				mp3Player.setPaused(false);
				return;
			} else {
				mp3Player.stopPlayer();
			}
		}
		if (trackIndex == tracks.size()) {
			buttons[PLAY_BUTTON_INDEX].setToolTipText("Play");
			buttons[PLAY_BUTTON_INDEX].setImage(LockerImages
					.get(LockerImages.PLAY));
			return;
		}
		Track trackToPlay = null;
		if (isShuffleEnabled()) {
			Integer indexToPlay = (Integer) shuffledIndices.get(trackIndex);
			trackToPlay = (Track) tracks.get(indexToPlay.intValue());
		} else {
			trackToPlay = (Track) tracks.get(trackIndex);
		}
		String label = trackToPlay.getArtistName()
				+ " - "
				+ trackToPlay.getTitle()
				+ " ("
				+ LockerDataUtils.getPrettyPrintedLength(trackToPlay
						.getLength()) + ")";
		titleLabel.setText(label);
		titleLabel.setToolTipText(label);
		if (albumArtThread != null) {
			albumArtThread.deactivate();
		}
		albumArtThread = new LoadAlbumArtThread(trackToPlay, canvas);
		albumArtThread.start();
		fireSelectionChanged(trackToPlay);
		mp3Player = new MP3Player(trackToPlay);
		mp3Player.addMP3PlayerListener(this);
		mp3Player.start();
		lastTrack = trackToPlay;
	}

	/** Stops playing the current song. */
	public void stop() {
		buttons[PLAY_BUTTON_INDEX].setToolTipText("Play");
		buttons[PLAY_BUTTON_INDEX]
				.setImage(LockerImages.get(LockerImages.PLAY));
		if (mp3Player != null) {
			mp3Player.stopPlayer();
		}
	}

	/** Pauses the current song. */
	private void pause() {
		buttons[PLAY_BUTTON_INDEX].setToolTipText("Play");
		buttons[PLAY_BUTTON_INDEX]
				.setImage(LockerImages.get(LockerImages.PLAY));
		if (mp3Player != null) {
			mp3Player.pausePlayer();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.abso.mp3tunes.locker.ui.IMP3PlayerListener#playerComplete()
	 */
	public void playerComplete() {
		if (lastTrack != null) {
			LockerPlugin.getDefault().submitToAudioScrobbler(lastTrack);
		}
		if (trackIndex < (tracks.size() - 1)) {
			trackIndex++;
			if (!buttons[PLAY_BUTTON_INDEX].isDisposed()) {
				Display display = buttons[PLAY_BUTTON_INDEX].getDisplay();
				display.asyncExec(new Runnable() {
					public void run() {
						play();
					}
				});
			}
		} else {
			if (!buttons[PLAY_BUTTON_INDEX].isDisposed()) {
				Display display = buttons[PLAY_BUTTON_INDEX].getDisplay();
				display.asyncExec(new Runnable() {
					public void run() {
						stop();
					}
				});
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.ISelectionProvider#addSelectionChangedListener(org.eclipse.jface.viewers.ISelectionChangedListener)
	 */
	public void addSelectionChangedListener(ISelectionChangedListener listener) {
		selectionChangedListeners.add(listener);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.ISelectionProvider#getSelection()
	 */
	public ISelection getSelection() {
		if ((trackIndex >= 0 && (trackIndex < tracks.size()))) {
			return new StructuredSelection(tracks.get(trackIndex));
		} else {
			return StructuredSelection.EMPTY;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.ISelectionProvider#removeSelectionChangedListener(org.eclipse.jface.viewers.ISelectionChangedListener)
	 */
	public void removeSelectionChangedListener(
			ISelectionChangedListener listener) {
		selectionChangedListeners.add(listener);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.ISelectionProvider#setSelection(org.eclipse.jface.viewers.ISelection)
	 */
	public void setSelection(ISelection selection) {
	}

	/**
	 * Notifies any selection changed listeners that the view's selection has
	 * changed. Only listeners registered at the time this method is called are
	 * notified.
	 * 
	 * @param trackToPlay
	 *            the track being played.
	 * 
	 * @see ISelectionChangedListener#selectionChanged
	 */
	private void fireSelectionChanged(Track trackToPlay) {
		final SelectionChangedEvent event = new SelectionChangedEvent(this,
				new StructuredSelection(trackToPlay));
		Object[] listeners = selectionChangedListeners.getListeners();
		for (int i = 0; i < listeners.length; ++i) {
			final ISelectionChangedListener l = (ISelectionChangedListener) listeners[i];
			SafeRunnable.run(new SafeRunnable() {
				public void run() {
					l.selectionChanged(event);
				}
			});
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.part.WorkbenchPart#getAdapter(java.lang.Class)
	 */
	public Object getAdapter(Class adapter) {
		if (adapter == IPropertySheetPage.class) {
			return new TabbedPropertySheetPage(this);
		} else {
			return super.getAdapter(adapter);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.part.WorkbenchPart#dispose()
	 */
	public void dispose() {
		if (mp3Player != null) {
			mp3Player.stopPlayer();
		}
		super.dispose();
	}

	/**
	 * A thread able to load an album art in background.
	 */
	private static class LoadAlbumArtThread extends Thread {

		/** The canvas where the album art image is displayed. */
		private Canvas canvas;

		/** The track whose album art must be retrieved. */
		private Track track;

		/**
		 * A flag which is set to <code>false</code> when the track this
		 * thread is associated with is no longer the current track of the music
		 * player.
		 */
		private boolean active;

		/**
		 * Constructs a new thread.
		 * 
		 * @param track
		 *            the track whose album art must be retrieved.
		 * @param canvas
		 *            the canvas where the album art image is displayed.
		 */
		public LoadAlbumArtThread(Track track, Canvas canvas) {
			this.track = track;
			this.canvas = canvas;
			this.active = true;
		}

		/**
		 * Invoked to indicate that the associated track is no longer the
		 * current player's track.
		 */
		public void deactivate() {
			this.active = false;
		}

		/* (non-Javadoc)
		 * @see java.lang.Thread#run()
		 */
		public void run() {
			if (!active) {
				return;
			}
			try {
				byte[] bytes = track.getAlbumArtAsBytes();
				if (!active) {
					return;
				}
				final ImageData imageData = new ImageData(
						new ByteArrayInputStream(bytes));
				if (!canvas.isDisposed()) {
					canvas.getDisplay().asyncExec(new Runnable() {
						public void run() {
							if (active) {
								Image image = new Image(canvas.getDisplay(),
										imageData.scaledTo(
												canvas.getBounds().width,
												canvas.getBounds().height));
								Image oldImage = canvas.getBackgroundImage();
								canvas.setBackgroundImage(image);
								if (oldImage != null) {
									oldImage.dispose();
								}
							}
						}
					});
				}
			} catch (Throwable e) {
				if (!canvas.isDisposed()) {
					canvas.getDisplay().asyncExec(new Runnable() {
						public void run() {
							if (active) {
								canvas.setBackgroundImage(null);
							}
						}
					});
				}
			}
		}

	}

}
